<?php
/**
 * Http请求客户端
 * 
 * <code>
 *  $http = new IHttpClient("http://my/test/response/index?a=b");
 *	$http->setBody("a=b&c=d&e=f");
 *	$http->setContentType("application/x-www-form-urlencoded");
 *	$response = $http->request(IHttpClient::METHOD_POST);
 *	print_r($response->body());
 *	</code>
 * 
 * 结果可能是：
 * <code>
 * Array
 * (
 *     [a] => b
 *     [c] => d
 *     [e] => f
 * )
 * 
 * </code>
 * 
 * Header头说明可以在 http://en.wikipedia.org/wiki/List_of_HTTP_headers 查阅
 *
 * @version $Id$
 * @package if
 * @subpackage util.net
 * @since 1.0
 */
class IHttpClient extends IObject {
	private $headers = array();
	private $persist;
	private $body;
	const METHOD_GET = "GET";
	const METHOD_POST = "POST";
	const METHOD_PUT = "PUT";//1.1.0
	const METHOD_DELETE = "DELETE";//1.1.0
	
	const AUTH_BASIC  = "Basic";//1.1.1
    const AUTH_DIGEST = "Digest";//1.1.1
	
	/**
	 * Enter description here...
	 *
	 * @var IURI
	 */
	private $uri;
	private $timeout = 30;
	private static $connections = array();
	private $httpVersion;//v1.1.0
	/**
	 * "主"请求的响应
	 *
	 * @var IHttpResponse
	 */
	private $response;//v1.1.0
	/**
	 * 同一个对话中中的cookie
	 *
	 * @var array
	 */	
	private $cookies = array();//v1.1.0
	
	private $proxy = array();
	
	/**
	 * 构造器
	 * 
	 * 如果你想直接请求一个域名的IP，而不是域名，可以使用：
	 * <code>
	 * $http = new IHttpClient("http://192.167.0.1/a/b/c");
	 * $http->addHeader("Host", "mydomain.com");
	 * $http->request();
	 * </code>
	 *
	 * @param string|IURI $uri 目标URI，1.1.0支持IURI对象
	 * @since 1.0
	 */
	function __construct($uri) {
		if (if_is_instance_of($uri, "IURI")) {
			$this->uri = $uri;
		}
		else {
			$this->uri = new IURI($uri);
		}
		if ($this->uri->user()) {
			$this->setAuth($this->uri->user(), $this->uri->pass());
		}
	}
	
	/**
	 * 设置发送的主体
	 *
	 * @param string|array $body 主体，v1.1.0支持数组
	 * @since 1.0
	 */
	function setBody($body) {
		if (is_array($body)) {
			$this->body = IURI::encodeQuery($body);
		}
		else {
			$this->body = $body;
		}
	}
	
	/**
	 * 设置发送的内容类型
	 *
	 * 比如
	 * - post: application/x-www-form-urlencoded
	 * - xml: text/xml;charset=UTF-8
	 * - ...
	 * 
	 * @param string $contentType 内容类型
	 * @since 1.0
	 */
	function setContentType($contentType) {
		$this->headers["Content-Type"] = $contentType;
	}
	
	/**
	 * 设置是否持久连接
	 * 
	 * IHttpClinet不能处理持久连接
	 *
	 * @param boolean $persist 是否持久连接
	 * @since 1.0
	 */
	function setPersist($persist) {
		$this->persist = $persist;
	}
	
	/**
	 * 添加Header报头
	 *
	 * @param string $nameOrHeaders 字段名称或字段名值对，从v1.1.0起支持加入一组字段名值对
	 * @param string|integer $value 值
	 * @since 1.0
	 */
	function addHeader($nameOrHeaders, $value = null) {
		if (is_array($nameOrHeaders)) {
			$this->headers = array_merge($this->headers, $nameOrHeaders);
		}
		else {
			$this->headers[$nameOrHeaders] = $value;
		}
	}
	
	/**
	 * 设置Header报头
	 *
	 * @param array $headers 一组报头
	 * @since 1.0
	 * @see addHeader
	 */
	function setHeaders(array $headers) {
		$this->headers = $headers;
	}
	
	/**
	 * 设置连接超时时间
	 *
	 * @param integer $timeout 超时时间
	 * @since 1.0
	 */
	function setTimeout($timeout) {
		$this->timeout = intval($timeout);
	}
	
	/**
	 * 发送请求
	 * 
	 * 从v1.1.0开始，如果用POST方法发送请求时没有指定Content-Type，则自动设为
	 *    application/x-www-form-urlencoded
	 *
	 * @param string $method 请求方法
	 * @param string $httpVersion HTTP版本号
	 * @return IHttpResponse
	 * @since 1.0
	 */
	function request($method = self::METHOD_GET, $httpVersion = "1.1") {
		$connection = null;
		$errorNo = null;
		$errorString = null;
		
		//proxy
		$proxy = $this->_proxy();
		if ($proxy) {
			$connection = @fsockopen($proxy->host(), $proxy->port(), $errorNo, $errorString, $proxy->timeout());
			if ($connection) {
				if ($proxy->user()) {
					$this->addHeader("Proxy-Authorization", $proxy->authScheme() . " " . base64_encode($proxy->user() . ":" . $proxy->password()));
				}
				//$this->addHeader("Host", $proxy->host());
			}
			else {
				throw new INetException("Failed to open proxy connection. Host: " . $proxy->host() . ", Port: " . $proxy->port() . ", {$errorString}", $errorNo);
			}
		}
		
		if (!isset($this->headers["Host"])) {
			$this->addHeader("Host", $this->uri->host());
		}
		
		//content type
		if ($method == self::METHOD_POST) {
			if (!isset($this->headers["Content-Type"])) {
				$this->addHeader("Content-Type", "application/x-www-form-urlencoded");
			}
		}
		$this->httpVersion = $httpVersion;
		$host = $this->uri->host();
		$port = $this->uri->port();
		$realhost = $this->uri->isSecure() ? "ssl://" . $host : $host;
		if (!$connection) {//no proxy
			if (!$this->persist) {
				$connection = @fsockopen($realhost, $port, $errorNo, $errorString, $this->timeout);
			}
			else {
				if (!isset(self::$connections[$realhost . ":" . $port])) {
					self::$connections[$realhost . ":" . $port] = @pfsockopen($realhost, $port, $errorNo, $errorString, $this->timeout);
				}
				$connection = &self::$connections[$realhost . ":" . $port];
			}
			
			if (!$connection) {
				throw new INetException("Failed to open http connection. Host: {$host}, Port: {$port}, {$errorString}", $errorNo);
			}
		}
		
		$out = "{$method} " . ($proxy ? $this->uri->__toString() : $this->uri->path()) . ($this->uri->query() ? "?" .  $this->uri->query():"") . " HTTP/{$httpVersion}\r\n";
		foreach ($this->headers as $name=>$value) {
			$out .= "{$name}: {$value}\r\n";
		}
	
		if (!is_null($this->body)) {
			$body = $this->body;
			$out .= "Content-Length: " . strlen($body) . "\r\n";
		}	
		if (!isset($this->headers["Connection"])) {
			$out .= "Connection: Close\r\n";
		}
		$out .= "\r\n";
		if (!is_null($this->body)) {
			$body = ($this->body);
			$out .= $body . "\r\n";
		}
		//p($out);
		fwrite($connection, $out);
		
		$responseString = "";
		while (!feof($connection)) {
			$line = fread($connection, 4096);
			$responseString .= $line;
		}
		
		if (!$this->persist) {
			fclose($connection);
		}
		
		if (!$responseString) {
			throw new INetException("Response from server {$host}:{$port} is empty", 0x100110);
		}
		
		$this->response = new IHttpResponse($responseString);
		$this->cookies = array_merge($this->cookies, $this->response->cookies());
		return $this->response;
	}
	
	/**
	 * 跳转到新的地址
	 * 
	 * 在同一个客户端对象（IHttpClient）下的跳转过程中将保持会话，这意味着你可以先
	 * 通过登录页面登录后，再在同一个会话中访问其他页面。
	 *
	 * @param string $uri 路径，可以不包含host部分
	 * @param string $method 请求方法
	 * @param string|array $body 信息主题或者信息参数名值对
	 * @param array $headers 报头信息
	 * @param boolean $cookie 是否保留cookie
	 * @return IHttpResponse
	 * @since 1.1.0
	 */
	function redirect($uri, $method = self::METHOD_GET, $body = null, $headers = array(), $cookie = true) {
		$uri = new IURI($uri);
		if (!$uri->host()) {
			$uri->set("host", $this->uri->host());
		}
		if (!$uri->port()) {
			$uri->set("port", $this->uri->port());
		}
		$client = new IHttpClient($uri);
		if ($this->proxy) {
			foreach ($this->proxy as $_proxy) {
				$client->addProxy($_proxy["obj"], $_proxy["weight"]);
			}
		}		
		$_headers = $this->headers;
		if (is_array($headers)) {
			$_headers = array_merge($_headers, $headers);
		}
		unset($_headers["Content-Type"]);
		$client->addHeader($_headers);
		
		//保持Cookie
		if ($cookie && !empty($this->cookies)) {
			$cookies = array();
			foreach ($this->cookies as $_cookie) {
				$cookies[$_cookie->name()] = $_cookie->name() . "=" . $_cookie->value();
			}
			if (!empty($cookies)) {
				$cookies = implode(";", $cookies);
				if (isset($headers["Cookie"])) {
					$cookies = $cookies . ";" . $headers["Cookie"];
				}
				$client->addHeader("Cookie", $cookies);
			}
		}
		
		$client->setBody($body);
		$response = $client->request($method, $this->httpVersion);
		//p($uri->__toString(), $client->headers(), $response->headers(), "==================");
		$this->cookies = array_merge($this->cookies, $response->cookies());
		return $response;
	}
	
	
	/**
	 * 取得当前请求的响应对象
	 *
	 * @return IHttpResponse
	 * @since 1.1.0
	 */
	function response() {
		return $this->response;	
	}
	
	/**
	 * 取得客户端发送的请求报头
	 *
	 * @return array
	 * @since 1.1.1
	 */
	function headers() {
		return $this->headers;
	}
	
	/**
	 * 取得客户端使用的HTTP协议版本
	 * 
	 * 只有在请求结束后才能得到
	 *
	 * @return string
	 * @since 1.1.1
	 */
	function httpVersion() {
		return $this->httpVersion;
	}
	
	/**
	 * 设置客户端使用的HTTP协议版本
	 *
	 * @param string $httpVersion HTTP协议版本
	 * @since 1.1.1
	 */
	function setHttpVersion($httpVersion) {
		$this->httpVersion = $httpVersion;
	}
	
	/**
	 * 取得请求的URI对象
	 *
	 * @return IURI
	 * @since 1.1.1
	 */
	function uri() {
		return $this->uri;
	}
	
	private function _header($name) {
		return isset($this->headers[$name]) ? $this->headers[$name] : null;
	}
	
	/**
	 * 增加一个Cookie
	 *
	 * @param string $name Cookie名
	 * @param string $value Cookie值
	 * @since 1.1.1
	 */
	function addCookie($name, $value) {
		$cookie = $this->_header("Cookie");
		$this->headers["Cookie"] = $cookie ? $cookie . ";{$name}={$value}" : "{$name}={$value}";
	}
	
	/**
	 * 设置HTTP认证的参数
	 * 
	 * 目前只支持Basic认证
	 *
	 * @param unknown_type $username
	 * @param unknown_type $password
	 * @param unknown_type $schema
	 * @since 1.1.1
	 */
	function setAuth($username, $password = null, $scheme = self::AUTH_BASIC) {
		$this->headers["Authorization"] = $scheme . " " . base64_encode($username . ":" . $password);
	}
	
	/**
	 * 添加一个代理
	 * 
	 * <code>
	 * $http->addProxy(new IHttpProxy($proxy1, $port1));
	 * $http->addProxy(new IHttpProxy($proxy2, $port2));
	 * </code>
	 * 
	 * 如果一个客户端设置了多个代理，那么可以根据$weight值进行随机选择一个作为当前使用的代理。
	 *
	 * @param IHttpProxy $proxy 代理对象，可以使用IHttpProxy类构造
	 * @param integer $weight 该代理对象的权重，小于1则永远不被使用到
	 * @since 1.1.1
	 */
	function addProxy(IHttpProxy $proxy, $weight = 1) {
		$this->proxy[] = array( "obj" => $proxy, "weight" => intval($weight) );
	}
	
	/**
	 * 删除一个代理
	 *
	 * @param string $name 代理名，要和addProxy()中的名字一致。如果$name为null，则视为删除所有代理
	 * @since 1.1.1
	 */
	function removeProxy($name = null) {
		if (is_null($name)) {
			$this->proxy = array();
		}
		else {
			foreach ($this->proxy as $key => $value) {
				if ($value["obj"]->name() == $name) {
					unset($this->proxy[$key]);
				}
			}
		}
	}
	
	/**
	 * 选取一个proxy
	 * 
	 * 目前是根据proxy的权重随机选取。
	 *
	 * @return IHttpProxy
	 * @since 1.1.1
	 */
	private function _proxy() {
		if (empty($this->proxy)) {
			return null;
		}
		$weights = if_array_pick($this->proxy, "weight");
		$key = $this->_randomKeyByWeight($weights);
		return $this->proxy[$key]["obj"];
	}
	
	/**
	 * Enter description here...
	 *
	 * @param unknown_type $weights
	 * @return unknown
	 * @todo 算法需要改进
	 * @since 1.1.1
	 */
	private function _randomKeyByWeight($weights) {
		$array = array();
		foreach ($weights as $key => $weight) {
			$array = array_pad($array, count($array) + $weight, $key);
		}
		return $array[array_rand($array, 1)];
	}
}

?>